package gecp.edge.image;

import gecp.edge.Detector;
import gecp.edge.Location;
import gecp.math.Angle;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;

/**
 * Image
 *
 * @author Cosmin
 */
public class Image {

    /**
     * The step for computations
     */
    public int step;
    /**
     * The thresholder for computations
     */
    public double thresholder;
    /**
     * The image filename
     */
    protected String _imageName;
    /**
     * The image
     */
    protected BufferedImage _image = null;
    /**
     * The color matrix of the image
     */
    public int[][] color;
    /**
     * The edge flag matrix
     */
    public boolean[][] isEdge;
    /**
     * The image matrix width
     */
    public int width;
    /**
     * The image matrix height
     */
    public int height;
    /**
     * The average forces amplitude
     */
    public double avgAmplitude;
    /**
     * The matrix of force amplitudes
     */
    public double[][] forceAmplitude;
    /**
     * The matrix of force angles
     */
    public double[][] forceAngle;
    /**
     * The sub image for processing the forces
     */
    protected SubImage _subImage;

    /**
     * Create a new Image object
     *
     * @param imageName The image false name
     */
    public Image(String imageName) {
        this._imageName = imageName;
        this._loadImage();
    }

    /**
     * Load the image from HDD and convert it to gray scale and extract the
     * color matrix
     */
    private void _loadImage() {
        try {
            /**
             * Read the image from HDD
             */
            BufferedImage image = ImageIO.read(new File(this._imageName));
            this.width = image.getWidth();
            this.height = image.getHeight();
            this._image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
            /**
             * Convert image to gray scale
             */
            Graphics g = this._image.getGraphics();
            g.drawImage(image, 0, 0, null);
            g.dispose();
            /**
             * Extract colors from gray scale image
             */
            this.color = new int[this.width][this.height];
            for (int i = 0; i < this.width; i++) {
                for (int j = 0; j < this.height; j++) {
                    Color _color = new Color(this._image.getRGB(i, j));
                    /**
                     * Get only the red color because the image is gray scale
                     * and R === G === B
                     */
                    this.color[i][j] = _color.getRed();
                }
            }
        } catch (Exception ex) {
            Logger.getLogger(Image.class.getName()).log(Level.SEVERE, null, ex);
            this._image = null;
        }
    }

    /**
     * Building the sub image for processing the forces
     */
    protected void _buildSubImage() {
        try {
            this._subImage = new SubImage(this.width, this.height, 0, 0);
            this._subImage.buildSubImage(this.color);
        } catch (Exception ex) {
            Logger.getLogger(Image.class.getName()).log(Level.SEVERE, null, ex);
            this._subImage = null;
        }
    }

    /**
     * Compute the forces with the sub image
     *
     * @param step The step for computations
     * @param thresholder The thresholder for the computations
     */
    protected void _computeSubImage(int step, double thresholder) {
        try {
            if (null != this._subImage) {
                this._subImage.computeSubImage(this.width, this.height, 0, 0, step, thresholder);
            }
        } catch (Exception ex) {
            Logger.getLogger(Image.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * Extract the forces amplitudes and angles matrix from sub image
     */
    protected void _extractFromSubImage() {
        try {
            this.forceAmplitude = new double[this.width][this.height];
            this.forceAngle = new double[this.width][this.height];
            for (int i = 0; i < this.width; i++) {
                for (int j = 0; j < this.height; j++) {
                    this.forceAmplitude[i][j] = this._subImage.forceAmplitude[i][j];
                    this.forceAngle[i][j] = this._subImage.forceAngle[i][j];
                }
            }
        } catch (Exception ex) {
            Logger.getLogger(Image.class.getName()).log(Level.SEVERE, null, ex);
        }

    }

    /**
     * Compute if a pixel is on a edge
     *
     * @param x The coordinate on x axis
     * @param y The coordinate on y axis
     * @param thresholder The thresholder for computing
     *
     * @return If the pixel is on a edge
     */
    protected boolean _computeIsEdge(int x, int y, double thresholder) {
        boolean _pixelIsOnEdge = false;
        if (this.forceAmplitude[x][y] >= thresholder) {
            try {
                int adjustedAngle = Angle.adjustAngle(this.forceAngle[x][y]);
                int px = x;
                int py = y;
                switch (adjustedAngle) {
                    case 0:
                        px++;
                        break;
                    case 45:
                        px++;
                        py++;
                        break;
                    case 90:
                        py++;
                        break;
                    case 135:
                        px--;
                        py++;
                        break;
                    case 180:
                        px--;
                        break;
                    case 225:
                        px--;
                        py--;
                        break;
                    case 270:
                        py--;
                        break;
                    case 315:
                        px++;
                        py--;
                        break;
                }
                if (px >= 0 && px < this.width && py >= 0 && py < this.height) {
                    double a = Angle.affinityAngle(this.forceAngle[x][y] - this.forceAngle[px][py]);
                    if (a >= 90 && a <= 270) {
                        _pixelIsOnEdge = true;
                    }
                }
            } catch (Exception ex) {
                Logger.getLogger(Image.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        return _pixelIsOnEdge;
    }

    /**
     * Compute the edge matrix
     *
     * @param thresholder The thresholder for computing
     */
    protected void _computeEdgeMatrix(double thresholder) {
        this.isEdge = new boolean[this.width][this.height];
        for (int i = 0; i < this.width; i++) {
            for (int j = 0; j < this.height; j++) {
                this.isEdge[i][j] = this._computeIsEdge(i, j, thresholder);
            }
        }
    }

    /**
     * Compute the avg amplitude
     */
    protected void _computeAvgAmplitude() {
        double totalAmplitude = 0;
        for (int i = 0; i < this.width; i++) {
            for (int j = 0; j < this.height; j++) {
                totalAmplitude += this.forceAmplitude[i][j];
            }
        }
        this.avgAmplitude = totalAmplitude / (this.width * this.height);
        System.out.println("Average amplitude is " + this.avgAmplitude);
    }

    /**
     * Generate and save to HDD a PNG file with all the pixel on edges
     *
     * @param filename The file to save
     */
    protected void _generateEdgePNG(String filename) {
        if (!Detector.usePNG) {
            return;
        }
        System.out.println("Start render the edge png of " + this.width + "x" + this.height);
        BufferedImage bi = new BufferedImage(this.width, this.height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D ig2 = bi.createGraphics();
        try {
            for (int i = 0; i < this.width; i++) {
                for (int j = 0; j < this.height; j++) {
                    if (this.isEdge[i][j]) {
                        ig2.setColor(Color.RED);
                        ig2.drawRect(i, j, 1, 1);
                    }
                }
            }
            Detector.writePNGToHDD(filename, bi);
        } catch (Exception ex) {
            Logger.getLogger(Image.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * Generate and save to HDD a PNG file with all pixel and forces
     *
     * @param filename The file to save
     * @param divisor The divisor of forces
     */
    protected void _generateForcePNG(String filename, double divisor) {
        if (!Detector.usePNG) {
            return;
        }
        int amplify = 15;
        System.out.println("Start render the output png of " + (this._image.getWidth() * amplify) + "x" + (this._image.getHeight() * amplify));
        BufferedImage bi = new BufferedImage(this._image.getWidth() * amplify, this._image.getHeight() * amplify, BufferedImage.TYPE_INT_ARGB);
        Graphics2D ig2 = bi.createGraphics();
        try {
            for (int i = 0; i < this._image.getWidth(); i++) {
                for (int j = 0; j < this._image.getHeight(); j++) {
                    int rgb = this.color[i][j] * 65536 + this.color[i][j] * 256 + this.color[i][j];
                    ig2.setColor(new Color(rgb));
                    ig2.fillRect(i * amplify, j * amplify, (i + 1) * amplify, (j + 1) * amplify);
                    try {
                        double amp = this.forceAmplitude[i][j] / divisor;
                        //double angle = Angle.adjustAngle(this.forceAngle[i][j]);
                        double angle = this.forceAngle[i][j];
                        int ix = (int) (i * amplify + (0.5 * amplify));
                        int jy = (int) (j * amplify + (0.5 * amplify));
                        ig2.setColor(Color.RED);
                        Location l = Angle.computeLocation(ix, jy, amp, angle);
                        ig2.drawLine(ix, jy, l.x, l.y);
                    } catch (Exception ex) {
                        Logger.getLogger(Image.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            }
            System.out.println("Drawing grid");
            ig2.setColor(Color.BLACK);
            for (int i = 0; i < this._image.getWidth(); i++) {
                ig2.drawLine(i * amplify, 0, i * amplify, this._image.getHeight() * amplify);
            }
            for (int i = 0; i < this._image.getHeight(); i++) {
                ig2.drawLine(0, i * amplify, this._image.getWidth() * amplify, i * amplify);
            }
            Detector.writePNGToHDD(filename, bi);
        } catch (Exception ex) {
            Logger.getLogger(Image.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * Compute the forces
     *
     * @param step The step for computations
     * @param thresholder The thresholder for the computations
     */
    public void compute(int step, double thresholder) {
        this.step = step;
        this.thresholder = thresholder;
        if (this._haveCache() && Detector.useCache) {
            System.out.println("Cache found. Loading data from cache CSV: " + this._getCacheFilename());
            this._readCacheFile();
            this._computeAvgAmplitude();
        } else {
            System.out.println("Cache not found. Computing data ...");
            System.out.println("build SubImage");
            this._buildSubImage();
            System.out.println("compute SubImage");
            this._computeSubImage(step, thresholder);
            System.out.println("extract FromSubImage");
            this._extractFromSubImage();
            System.out.println("building cache file");
            this._buildCacheFile();
            this._computeAvgAmplitude();
            this._generateForcePNG(this._imageName + "-2.forces." + step + ".png", this.avgAmplitude / 4);
        }
        System.out.println("compute Edge Matrix");
        this._computeEdgeMatrix(thresholder);
        this._generateForcePNG(this._imageName + "-2.forces." + step + ".png", this.avgAmplitude / 4);
        this._generateEdgePNG(this._imageName + "-2.edge." + step + ".png");
        System.out.println("Done");
    }

    /**
     * Building the cache CSV file for the current image and step
     */
    protected void _buildCacheFile() {
        if (!Detector.useCache) {
            return;
        }
        String cacheFilename = this._getCacheFilename();
        BufferedWriter writer = null;
        try {
            writer = new BufferedWriter(new FileWriter(cacheFilename));
            String line = this.width + "," + this.height + "," + this.step + "\n";
            writer.write(line);
            for (int i = 0; i < this.width; i++) {
                for (int j = 0; j < this.height; j++) {
                    try {
                        line = i + "," + j + "," + this.color[i][j] + "," + this.forceAmplitude[i][j] + "," + this.forceAngle[i][j] + "\n";
                        writer.write(line);
                    } catch (Exception ex) {
                        Logger.getLogger(Image.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            }
        } catch (IOException ex) {
            Logger.getLogger(Image.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            try {
                if (writer != null) {
                    writer.close();
                }
            } catch (IOException ex) {
                Logger.getLogger(Image.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    /**
     * Read data from cache CSV
     */
    protected void _readCacheFile() {
        String cacheFilename = this._getCacheFilename();
        try {
            FileReader fileReader = new FileReader(new File(cacheFilename));
            BufferedReader bufferedReader = new BufferedReader(fileReader);
            String line;
            boolean firstLine = true;
            while ((line = bufferedReader.readLine()) != null) {
                String[] items = line.split(",");
                if (firstLine) {
                    if (items.length == 3) {
                        this.width = Integer.parseInt(items[0]);
                        this.height = Integer.parseInt(items[1]);
                        this.color = new int[this.width][this.height];
                        this.forceAmplitude = new double[this.width][this.height];
                        this.forceAngle = new double[this.width][this.height];
                    }
                    firstLine = false;
                } else {
                    if (items.length == 5) {
                        int x = Integer.parseInt(items[0]);
                        int y = Integer.parseInt(items[1]);
                        this.color[x][y] = Integer.parseInt(items[2]);
                        this.forceAmplitude[x][y] = Double.parseDouble(items[3]);
                        this.forceAngle[x][y] = Double.parseDouble(items[4]);
                    }
                }
            }
            fileReader.close();
        } catch (IOException ex) {
            Logger.getLogger(Image.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * Get the cache filename for the current file and step
     *
     * @return Cache filename
     */
    protected String _getCacheFilename() {
        File f = new File(this._imageName);
        String cacheFilename = "cache/" + f.getName() + ".cache." + this.step + ".csv";
        return cacheFilename;
    }

    /**
     * Check is the current file and step have cache CSV with forces
     *
     * @return If cache file exists
     */
    protected boolean _haveCache() {
        File f = new File(this._getCacheFilename());
        return f.exists();
    }

    /**
     * Gets the size of the step
     *
     * @param step The step
     *
     * @return The size for the step
     */
    public static int getStepSize(int step) {
        switch (step) {
            case 1:
                return 1;
            case 2:
                return 5;
            case 3:
                return 20;
            case 4:
                return 40;
            case 5:
                return 80;
        }
        return 1;
    }

    /**
     * Get the step count for the step. The step count is the number of pixel to
     * step to next line of pixels
     *
     * @param step The step
     * @return
     */
    public static int getStepCount(int step) {
        switch (step) {
            case 3:
            case 4:
            case 5:
                return 1;
        }
        return 1;
    }
}
